#include <fmt/format.h>
#include "../opengl/opengl.hpp"
#include "../gl/Camera.hpp"
#include "../gl/Sphere.hpp"
#include "../gl/AxisHelper.hpp"
#include "../gl/Window.hpp"

namespace gl = opengl;

int main() {
    Window window("Model", 800, 600);
    gl::init_opengl(gl::opengl_config{.load_function = &SDL_GL_GetProcAddress,
                                      .debug_msg_callback = [](auto, auto, auto id, auto, auto, auto msg, auto) {
                                          fmt::println("[OpenGL object {}] {}", id, msg);
                                      }});
    AxisHelper helper(3.0);

    gl::vertex_array vao({
        gl::vertex_attribute(0, gl::type_f32, 3, 0),  // position
        gl::vertex_attribute(1, gl::type_f32, 3, 12), // normal
    });
    Sphere<64> mesh(1.0f);
    gl::buffer vertices(sizeof(mesh.vertices), mesh.vertices);
    gl::buffer indices(sizeof(mesh.indices), mesh.indices);
    gl::program program({gl::shader::from_file(gl::shader::vertex_shader, "../shader/caster/vs.glsl"),
                         gl::shader::from_file(gl::shader::fragment_shader, "../shader/caster/fs.glsl")});
    fmt::println("{}", program.get_error());

    PerspectiveCamera camera;
    camera.set_position({4.0f, 4.0f, -4.0f});
    camera.look_at(glm::vec3{0.0f});

    window.on_exec = [&](auto) {
        auto [w, h] = window.get_size();
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
    };
    window.on_resize = [&](auto, int w, int h) {
        gl::api::glViewport(0, 0, w, h);
        camera.set_aspect_ratio((float) w / (float) h);
    };

    // cameraPos: vec3 (0-16) matVP: mat4 (16-80)
    gl::buffer worldUBO(80); // binding = 0
    worldUBO.bind_to(gl::buffer::uniform_buffer, 0);

#pragma region DirLight
    constexpr gl::u32 NUM_MAX_DIRLIGHTS = 8;
    gl::u32 numDirLights = 1; // location = 5
    struct alignas(16) DirLight {
        glm::vec3 ambient{0.1f, 0.1f, 0.1f};
        gl::f32 __pad0__;
        glm::vec3 diffuse{0.6f, 0.6f, 0.6f};
        gl::f32 __pad1__;
        glm::vec3 specular{0.3f, 0.3f, 0.3f};
        gl::f32 __pad2__;
        glm::vec3 direction{0.0f, -1.0f, 0.0f};
    } dirLight[NUM_MAX_DIRLIGHTS]; // size: 64bytes
    gl::buffer dirLightUBO(sizeof(DirLight) * NUM_MAX_DIRLIGHTS);
    dirLightUBO.bind_to(gl::buffer::uniform_buffer, 1);
    dirLight[0].direction = glm::vec3(0.0f, -1.0f, 0.0f);
    dirLight[1].direction = glm::vec3(0.0f, 0.0f, 1.0f);
    // PointLight
    constexpr gl::u32 NUM_MAX_POINTLIGHTS = 8;
    gl::u32 numPointLights = 1; // location = 6
    struct alignas(16) PointLight {
        glm::vec3 ambient{0.1f, 0.1f, 0.1f};
        gl::f32 __pad0__;
        glm::vec3 diffuse{0.6f, 0.6f, 0.6f};
        gl::f32 __pad1__;
        glm::vec3 specular{0.3f, 0.3f, 0.3f};
        gl::f32 __pad2__;
        glm::vec3 position{4.0f, 0.0f, 0.0f};
        gl::f32 __pad3__;
        gl::f32 c0 = 1.0f, c1 = 0.09f, c2 = 0.032f;
    } pointLight[NUM_MAX_POINTLIGHTS]; // size: 80bytes
    gl::buffer pointLightUBO(sizeof(PointLight) * NUM_MAX_POINTLIGHTS);
    pointLightUBO.bind_to(gl::buffer::uniform_buffer, 2);
    // SpotLight
    constexpr gl::u32 NUM_MAX_SPOTLIGHTS = 8;
    gl::u32 numSpotLights = 1;
    struct alignas(16) SpotLight {
        glm::vec3 color{0.4f, 0.4f, 0.4f};
        float __pad0__;
        glm::vec3 position{2.0f, 2.0f, 0.0f};
        float __pad1__;
        glm::vec3 direction{-1.0f, -1.0f, 0.0f};
        gl::f32 cutOff = 10.0f; // In degrees
        glm::vec3 ambient{0.1f};
        gl::f32 outerCutOff = 20.0f; // In degrees
    } spotLight[NUM_MAX_SPOTLIGHTS];
    gl::buffer spotLightUBO(sizeof(SpotLight) * NUM_MAX_SPOTLIGHTS);
    spotLightUBO.bind_to(gl::buffer::uniform_buffer, 3);

    struct alignas(16) Transform {
        glm::mat4 modelMatrix;
        glm::mat4 mvpMatrix;
        glm::mat4 normalMatrix;
    } transform; // location = 0-2
    struct Material {
        glm::vec3 color{1.0f};
        float shiness = 32.0f;
    } material; // location = 3-4

    window.on_render = [&](auto) {
        // World
        worldUBO.set_data(glm::value_ptr(camera.position), sizeof(glm::vec3));
        worldUBO.set_data(glm::value_ptr(camera.viewProjectionMatrix), sizeof(glm::mat4), 16);
        // DirLight
        program.set_uint32(5, numDirLights);
        dirLightUBO.set_data(&dirLight, sizeof(DirLight) * numDirLights);
        // PointLight
        program.set_uint32(6, numPointLights);
        pointLightUBO.set_data(&pointLight, sizeof(PointLight) * numPointLights);
        // SpotLight
        program.set_uint32(7, numSpotLights);
        spotLightUBO.set_data(&spotLight, sizeof(SpotLight) * numSpotLights);
        // Transform
        program.set_mat4f(1, glm::value_ptr(camera.viewProjectionMatrix)); // mvpMatrix;
        // Material
        program.set_vec3f(3, glm::value_ptr(material.color));
        program.set_float32(4, material.shiness);
        // Render
        gl::clear_buffers();
        helper.render();
        vao.bind();
        program.use();
        vao.set_vertex_buffer(vertices, sizeof(Sphere<64>::vertex));
        vao.set_index_buffer(indices);
        gl::api::glDrawElements(gl::triangles, Sphere<64>::num_indices, gl::type_u32, nullptr);
    };
    window.on_process_event = [&](auto, const SDL_Event *event) {
        if (event->type == SDL_EventType::SDL_MOUSEWHEEL) {
            constexpr float sensitivity = 2.0f;
            auto fov = glm::clamp(camera.fov - sensitivity * event->wheel.preciseY, 15.0f, 85.0f);
            camera.set_fov(fov);
        }
    };

    return window.exec();
}